/*
 *
 *  Copyright (c) 2022 mobile.dev inc.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 *
 */

package maestro.cli.command

import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import maestro.TreeNode
import maestro.cli.App
import maestro.cli.DisableAnsiMixin
import maestro.cli.ShowHelpMixin
import maestro.cli.analytics.Analytics
import maestro.cli.analytics.PrintHierarchyFinishedEvent
import maestro.cli.analytics.PrintHierarchyStartedEvent
import maestro.cli.report.TestDebugReporter
import maestro.cli.session.MaestroSessionManager
import maestro.cli.view.yellow
import maestro.utils.CliInsights
import maestro.utils.Insight
import maestro.utils.chunkStringByWordCount
import picocli.CommandLine
import picocli.CommandLine.Option
import java.lang.StringBuilder

@CommandLine.Command(
    name = "hierarchy",
    description = [
        "Print out the view hierarchy of the connected device"
    ],
    hidden = true
)
class PrintHierarchyCommand : Runnable {

    @CommandLine.Mixin
    var disableANSIMixin: DisableAnsiMixin? = null

    @CommandLine.Mixin
    var showHelpMixin: ShowHelpMixin? = null

    @CommandLine.ParentCommand
    private val parent: App? = null

    @CommandLine.Option(
        names = ["--android-webview-hierarchy"],
        description = ["Set to \"devtools\" to use Chrome dev tools for Android WebView hierarchy"],
        hidden = true,
    )
    private var androidWebViewHierarchy: String? = null

    @CommandLine.Option(
        names = ["--reinstall-driver"],
        description = ["[Beta] Reinstalls xctestrunner driver before running the test. Set to false if the driver shouldn't be reinstalled"],
        hidden = true
    )
    private var reinstallDriver: Boolean = true

    @Option(
        names = ["--apple-team-id"],
        description = ["The Team ID is a unique 10-character string generated by Apple that is assigned to your team's apple account."],
        hidden = true
    )
    private var appleTeamId: String? = null

    @CommandLine.Option(
        names = ["--compact"],
        description = ["Output in CSV format with element_num,depth,attributes,parent_num columns"],
        hidden = false
    )
    private var compact: Boolean = false

    @CommandLine.Option(
        names = ["--device-index"],
        description = ["The index of the device to run the test on"],
        hidden = true
    )
    private var deviceIndex: Int? = null

    override fun run() {
        TestDebugReporter.install(
            debugOutputPathAsString = null,
            flattenDebugOutput = false,
            printToConsole = parent?.verbose == true,
        )
        
        // Track print hierarchy start
        val platform = parent?.platform ?: "unknown"
        val startTime = System.currentTimeMillis()
        Analytics.trackEvent(PrintHierarchyStartedEvent(platform = platform))
        

        MaestroSessionManager.newSession(
            host = parent?.host,
            port = parent?.port,
            driverHostPort = null,
            teamId = appleTeamId,
            deviceId = parent?.deviceId,
            platform = parent?.platform,
            reinstallDriver = reinstallDriver,
            deviceIndex = deviceIndex
        ) { session ->
            session.maestro.setAndroidChromeDevToolsEnabled(androidWebViewHierarchy == "devtools")
            val callback: (Insight) -> Unit = {
                val message = StringBuilder()
                val level = it.level.toString().lowercase().replaceFirstChar(Char::uppercase)
                message.append(level.yellow() + ": ")
                it.message.chunkStringByWordCount(12).forEach { chunkedMessage ->
                    message.append("$chunkedMessage ")
                }
                println(message.toString())
            }
            val insights = CliInsights

            insights.onInsightsUpdated(callback)

            val tree = session.maestro.viewHierarchy().root

            insights.unregisterListener(callback)

            if (compact) {
                // Output in CSV format
                println("element_num,depth,attributes,parent_num")
                val nodeToId = mutableMapOf<TreeNode, Int>()
                val csv = StringBuilder()
                
                // Assign IDs to each node
                var counter = 0
                tree?.aggregate()?.forEach { node ->
                    nodeToId[node] = counter++
                }
                
                // Process tree recursively to generate CSV
                processTreeToCSV(tree, 0, null, nodeToId, csv)
                
                println(csv.toString())
            } else {
                // Original JSON output format
                val hierarchy = jacksonObjectMapper()
                    .setSerializationInclusion(JsonInclude.Include.NON_NULL)
                    .writerWithDefaultPrettyPrinter()
                    .writeValueAsString(tree)
                
                println(hierarchy)
            }
        }
        
        // Track successful completion
        val duration = System.currentTimeMillis() - startTime
        Analytics.trackEvent(PrintHierarchyFinishedEvent(
            platform = platform,
            success = true,
            durationMs = duration
        ))
        Analytics.flush()
    }
    
    private fun processTreeToCSV(
        node: TreeNode?, 
        depth: Int, 
        parentId: Int?, 
        nodeToId: Map<TreeNode, Int>,
        csv: StringBuilder
    ) {
        if (node == null) return
        
        val nodeId = nodeToId[node] ?: return
        
        // Build attributes string
        val attributesList = mutableListOf<String>()
        
        // Add normal attributes
        node.attributes.forEach { (key, value) ->
            if (value.isNotEmpty() && value != "false") {
                attributesList.add("$key=$value")
            }
        }
        
        // Add boolean properties if true
        if (node.clickable == true) attributesList.add("clickable=true")
        if (node.enabled == true) attributesList.add("enabled=true")
        if (node.focused == true) attributesList.add("focused=true")
        if (node.checked == true) attributesList.add("checked=true")
        if (node.selected == true) attributesList.add("selected=true")
        
        // Join all attributes with "; "
        val attributesString = attributesList.joinToString("; ")
        
        // Escape quotes in the attributes string if needed
        val escapedAttributes = attributesString.replace("\"", "\"\"")
        
        // Add this node to CSV
        csv.append("$nodeId,$depth,\"$escapedAttributes\",${parentId ?: ""}\n")
        
        // Process children
        node.children.forEach { child ->
            processTreeToCSV(child, depth + 1, nodeId, nodeToId, csv)
        }
    }

    private fun removeEmptyValues(tree: TreeNode?): TreeNode? {
        if (tree == null) {
            return null
        }

        return TreeNode(
            attributes = tree.attributes.filter {
                it.value != "" && it.value.toString() != "false"
            }.toMutableMap(),
            children = tree.children.map { removeEmptyValues(it) }.filterNotNull(),
            checked = if(tree.checked == true) true else null,
            clickable = if(tree.clickable == true) true else null,
            enabled = if(tree.enabled == true) true else null,
            focused = if(tree.focused == true) true else null,
            selected = if(tree.selected == true) true else null,
        )
    }
}
